//
//  Connection.m
//  Networking
//
//  Created by Christoph Eßer on 11/24/11.
//  Copyright 2011 __MyCompanyName__. All rights reserved.
//

#import "Connection.h"
#import <netinet/in.h>
#import <sys/socket.h>
#include <arpa/inet.h>
#import "NetworkMessage.h"
#import <Foundation/Foundation.h>
#import "NetworkMessageHandler.h"

// C-function used by socket csllback, just calls class callback
// method with data object as argument
static void dataCallBack(CFSocketRef s, CFSocketCallBackType type, CFDataRef address, const void *data, void *info) {
    assert(type == kCFSocketDataCallBack);

    id refToSelf = (id) info;
    //call member function
    [refToSelf callBackWithData: data];
}

@implementation Connection
@synthesize delegate, noError;    

-(id) initWithNativeSocket: (CFSocketNativeHandle) socketFD {
    self = [super init];
        
    if ( self ) {
        noError = true;
        
        //create socket context to provide self as info varieble in callback
        CFSocketContext socketCtxt = {0, self, NULL, NULL, NULL};
        //create socket with callback and insert into runloop
        socketCF = CFSocketCreateWithNative(NULL, socketFD, (kCFSocketDataCallBack), dataCallBack, &socketCtxt);
        CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(NULL, socketCF, 0);
        CFRunLoopAddSource(CFRunLoopGetCurrent(),source,kCFRunLoopDefaultMode);
        CFRelease(source);
    }
    
    return self;
}
    
-(id) initWithRemoteAddress: (NSString*) addr andPort: (NSInteger) port{
    self = [super init];
        
    if ( self ) {
        noError = true;
        
        //This code is basically copied from Wikipedia...
        //see http://en.wikipedia.org/wiki/Berkeley_sockets#Client
        NSLog(@"Connecting to server %@:%ld", addr, port);
            
        struct sockaddr_in stSockAddr;
        int Res;
        int SocketFD = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
            
        if (-1 == SocketFD)
        {	
            NSLog(@"cannot create socket");
            noError = false;      
        }
            
        memset(&stSockAddr, 0, sizeof(stSockAddr));
            
        stSockAddr.sin_family = AF_INET;
        stSockAddr.sin_port = htons(port);
        Res = inet_pton(AF_INET, [addr UTF8String], &stSockAddr.sin_addr);
            
        if (0 > Res)
        {
            NSLog(@"error: first parameter is not a valid address family");
            close(SocketFD);
            noError = false;
        }   
        else if (0 == Res)
        {
            NSLog(@"char string (second parameter does not contain valid ipaddress)");
            close(SocketFD);
            noError = false;
        }
            
        if (-1 == connect(SocketFD, (struct sockaddr *)&stSockAddr, sizeof(stSockAddr)))
        {
            NSLog(@"connect failed");
            close(SocketFD);
            noError = false;
        }
            
        //create socket context to provide self as info variable in callback
        CFSocketContext socketCtxt = {0, self, NULL, NULL, NULL};
        //create socket with callback and insert into runloop
        socketCF = CFSocketCreateWithNative(NULL, (CFSocketNativeHandle) SocketFD, (kCFSocketDataCallBack), dataCallBack, &socketCtxt);
        CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(NULL,  socketCF, 0);
        CFRunLoopAddSource(CFRunLoopGetCurrent(),source,kCFRunLoopDefaultMode);
        CFRelease(source);
    }
        
    return self;
}

-(void) dealloc {
    CFSocketInvalidate(socketCF);
    CFRelease(socketCF);
}

-(NSString*) getRemoteAddress {
    //use some CF functions to extract remote address information from socket
    CFDataRef addr_data = CFSocketCopyPeerAddress(socketCF);
    struct sockaddr_in* addr = (struct sockaddr_in*)CFDataGetBytePtr(addr_data);
    char *ip_addr = inet_ntoa(addr->sin_addr);
    CFRelease(addr_data);
    return [NSString stringWithUTF8String: ip_addr];
}

-(NSInteger) getRemotePort {
    //use some CF functions to extract remote port information from socket
    CFDataRef addr_data = CFSocketCopyPeerAddress(socketCF);
    struct sockaddr_in* addr = (struct sockaddr_in*)CFDataGetBytePtr(addr_data);
    u_short port = ntohs(addr->sin_port);
    CFRelease(addr_data);
    return port;
}

-(NSInteger) getLocalPort {
    //use some CF functions to extract local port information from socket
    CFDataRef addr_data = CFSocketCopyAddress(socketCF);
    struct sockaddr_in* addr = (struct sockaddr_in*)CFDataGetBytePtr(addr_data);
    u_short port = ntohs(addr->sin_port);
    CFRelease(addr_data);
    return port;
}

-(BOOL) isAlive {
    //Is this sufficient? Maybe send a ping via socket and check for exceptions
    return CFSocketIsValid(socketCF);
}

// sends data via CFSocket functions
-(void) sendData:(NSData*)data {
    //five seconds timeout, maybe we need to adapt that to our needs
    CFTimeInterval timeout = 5.0;
    CFDataRef cfData = CFDataCreate(NULL,[data bytes],[data length]);
    CFSocketError retVal = CFSocketSendData (socketCF, NULL, cfData, timeout);
    switch (retVal) {
        case kCFSocketSuccess:
            NSLog(@"successful");
            break;
        case kCFSocketError:
            NSLog(@"ERROR"); //TODO: raise exception
            break;
        case kCFSocketTimeout:
            NSLog(@"Timeout"); //TODO: raise exception
            break;    
        default:
            break;
    }
    CFRelease(cfData);
}

// packs Message into NSData via NSKeyedArchiver
-(void) sendMessage: (NetworkMessage*) msg {
    NSData *data = [NSKeyedArchiver archivedDataWithRootObject: msg];
    [self sendData:data];
}

// mainly for debug purposes, sends string message
-(void) sendString: (NSString*) str {
    NetworkMessage *msg = [[NetworkMessage alloc] initWithType:kStringMessage msgId:0 data:str];
    [self sendMessage:msg];
    [msg release];
}

// callback method called by C callback function
-(void) callBackWithData: (const void*) data {
    NSData *decodedData =  [NSData dataWithBytes:CFDataGetBytePtr(data) length:CFDataGetLength(data)];
    
    //zero data length means connection aborted
    if([decodedData length] == 0) {
        [delegate connectionAborted:self];
    }
    else {
		//assumes only NetworkMessages are received 
        @try {
            NetworkMessage *msg = [[NSKeyedUnarchiver unarchiveObjectWithData: decodedData] retain];
            
            if(delegate != nil) {
                [delegate incomingMessage:msg fromConnection:self];
            }
            
            [msg release];
        }
        @catch (NSException *exception) {
            NSLog(@"Error in data callback: %@: %@", [exception name], [exception reason]);
        }

    }
};


@end
